package org.xx.armory.db.junit.impl;

import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xx.armory.db.DbException;
import org.xx.armory.db.impl.SimpleDataSource;
import org.xx.armory.db.junit.DbUnitHelper;
import org.xx.armory.db.junit.DbUnitTableReader;
import org.xx.armory.db.junit.DbUnitTableWriter;

import javax.sql.DataSource;
import javax.xml.stream.XMLStreamException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.sql.Types;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import static java.util.Base64.getDecoder;
import static java.util.Base64.getEncoder;
import static org.apache.commons.lang3.StringUtils.join;
import static org.apache.commons.lang3.StringUtils.substring;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.apache.commons.lang3.time.DateFormatUtils.format;
import static org.xx.armory.commons.SysUtils.dumpException;
import static org.xx.armory.commons.Validators.notBlank;
import static org.xx.armory.commons.Validators.notEmpty;
import static org.xx.armory.commons.Validators.notNull;

/**
 * 抽象的单元测试工具类，提供了用于单元测试的公共方法。
 */
public abstract class AbstractDbUnitHelper
        implements DbUnitHelper {
    private final Logger logger = LoggerFactory.getLogger(AbstractDbUnitHelper.class);
    private final DataSource dataSource;
    private final Map<String, DbUnitHelper.Column[]> tables;
    private final Map<String, DbUnitHelper.Row[]> tableRows;

    /**
     * 构造用于Oracle数据库的单元测试工具类。
     *
     * @param dataSource
     *         此工具类使用的数据源。
     * @throws NullPointerException
     *         如果参数{@code dataSource}是{@code null}。
     */
    protected AbstractDbUnitHelper(
            DataSource dataSource
    ) {
        notNull(dataSource, "dataSource");

        this.dataSource = dataSource;

        this.tables = new LinkedHashMap<>();
        this.tableRows = new LinkedHashMap<>();
    }

    /**
     * 构造{@code OracleDbUnitHelper}类型实例。
     *
     * @param url
     *         连接数据库的URL, 会被正规化。
     * @param user
     *         连接数据库的用户名, 会被正规化。
     * @param password
     *         连接数据库的口令, 会被正规化。
     * @throws NullPointerException
     *         如果参数{@code url}是{@code null}, 或者参数{@code user}是{@code null}。
     * @throws IllegalArgumentException
     *         如果参数{@code url}只包含空白字符, 或者参数{@code user}只包含空白字符。
     */
    protected AbstractDbUnitHelper(
            String url,
            String user,
            String password
    ) {
        final SimpleDataSource dataSource = new SimpleDataSource();
        dataSource.setDriverClassName("oracle.jdbc.driver.OracleDriver");
        dataSource.setUrl(notBlank(url, "url").trim());
        dataSource.setUsername(notBlank(user, "user").trim());
        dataSource.setPassword(trimToEmpty(password));

        this.dataSource = dataSource;

        this.tables = new LinkedHashMap<>();
        this.tableRows = new LinkedHashMap<>();
    }

    /**
     * 为执行INSERT命令的Statement对象设置参数。
     *
     * @param statement
     *         待设置参数的Statement对象。
     * @param columnIndex
     *         需要插入的列的序号(从1开始)。
     * @param columnType
     *         需要插入的列的类型。
     * @param columnValue
     *         需要插入的列的值。
     * @throws SQLException
     *         如果设置参数时出错。
     * @throws ParseException
     *         如果设置日期类型参数时，列的值无法被解析为日期类型。
     * @see #insert(String, DbUnitHelper.Column[], DbUnitHelper.Row[])
     */
    private static void parameter(
            PreparedStatement statement,
            int columnIndex,
            int columnType,
            String columnValue
    )
            throws SQLException, ParseException {
        if (columnValue == null || columnValue.length() == 0) {
            statement.setNull(columnIndex, columnType);
            return;
        }

        switch (columnType) {
            case Types.ARRAY:
                statement.setArray(columnIndex, null);
                break;
            case Types.BIGINT:
            case Types.DECIMAL:
            case Types.NUMERIC:
                statement.setBigDecimal(columnIndex, new BigDecimal(columnValue));
                break;
            case Types.BINARY:
            case Types.BLOB:
            case Types.VARBINARY:
            case Types.LONGVARBINARY:
                statement.setBytes(columnIndex, getDecoder().decode(columnValue));
                break;
            case Types.BIT:
            case Types.BOOLEAN:
                statement.setBoolean(columnIndex, "true".equals(columnValue) || "1".equals(columnValue));
                break;
            case Types.CHAR:
                statement.setString(columnIndex, substring(columnValue, 0, 1));
                break;
            case Types.DATE:
                statement.setDate(columnIndex, new java.sql.Date(DateUtils.parseDate(columnValue, "yyyy-MM-dd").getTime()));
                break;
            case Types.REAL:
            case Types.DOUBLE:
                statement.setDouble(columnIndex, Double.parseDouble(columnValue));
                break;
            case Types.FLOAT:
                statement.setFloat(columnIndex, Float.parseFloat(columnValue));
                break;
            case Types.INTEGER:
                statement.setInt(columnIndex, Integer.parseInt(columnValue));
                break;
            case Types.SMALLINT:
                statement.setShort(columnIndex, Short.parseShort(columnValue));
                break;
            case Types.TIME:
                statement.setTime(columnIndex, new java.sql.Time(DateUtils.parseDate(columnValue, "HH:mm:ss").getTime()));
                break;
            case Types.TIMESTAMP:
                statement.setTimestamp(columnIndex, new java.sql.Timestamp(DateUtils.parseDate(columnValue, "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss").getTime()));
                break;
            case Types.TINYINT:
                statement.setByte(columnIndex, Byte.parseByte(columnValue));
                break;
            case Types.VARCHAR:
            case Types.NCHAR:
            case Types.NVARCHAR:
            case Types.LONGVARCHAR:
            case Types.LONGNVARCHAR:
                statement.setString(columnIndex, columnValue);
                break;
            default:
                statement.setString(columnIndex, columnValue);
                break;
        }
    }

    /**
     * 将长整数转化为大整数对象。
     *
     * <p>
     * 检查参数{@code l}, 如果不是{@code null}则根据参数{@code l}构造{@code java.math.BigInteger}对象, 否则检查参数
     * {@code defInt}, 如果参数{@code defInt}是{@code null}或者只包含空白字符则返回{@code 0}, 否则通过{@code defInt}来构造
     * {@code java.math.BigInteger}对象并返回。
     * </p>
     *
     * @param l
     *         需要转化的长整数对象。
     * @param defaultValue
     *         缺省值
     * @return 转化后的大整数对象。
     * @throws NumberFormatException
     *         如果参数{@code l}是{@code null}, 并且参数{@code defInt}无法被表示为一个合法的整数。
     */
    protected static BigInteger checkBigInteger(
            Long l,
            String defaultValue
    ) {
        final String rd = defaultValue == null ? "" : defaultValue.trim();
        return l != null ? BigInteger.valueOf(l) :
                !rd.isEmpty() ? new BigInteger(rd) : BigInteger.ZERO;
    }

    /**
     * 将长整数转化为大整数对象。
     *
     * <p>
     * 检查参数{@code l}, 如果不是{@code null}则根据参数{@code l}构造{@code java.math.BigInteger}对象, 否则通过
     * {@code defInt}来构造 {@code java.math.BigInteger}对象并返回。
     * </p>
     *
     * @param l
     *         需要转化的长整数对象。
     * @param defaultValue
     *         缺省值
     * @return 转化后的大整数对象。
     */
    protected static BigInteger checkBigInteger(
            Long l,
            long defaultValue
    ) {
        return l != null ? BigInteger.valueOf(l) : BigInteger.valueOf(defaultValue);
    }

    /**
     * 创建数据库连接。
     *
     * @return 数据库连接。
     * @throws DbException
     *         如果创建数据库连接时出现错误。
     */
    protected final Connection connection()
            throws SQLException {
        final Connection conn = dataSource.getConnection();
        conn.setAutoCommit(false);
        return conn;
    }

    /**
     * 包装标识符，如果标识符和关键字重名，那么需要包装之后才能用于SQL语句中。
     *
     * @param identity
     *         待包装的标识符。
     * @return 包装结果。
     */
    protected String wrap(
            String identity
    ) {
        return identity;
    }

    /**
     * 构造执行INSERT记录的SQL命令。
     * <p>返回的SQL命令形如： {@literal INSERT INTO <table name> (<column name1>, <column name2>) VALUES (?, ?, ...) }</p>
     *
     * @param tableName
     *         表名。
     * @param columnNames
     *         列名数组。
     * @return 用于向指定的表INSERT记录的SQL命令。
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符，或者参数{@code columnNames}是{@code null}或者长度为0，
     *         或者参数{@code columnNames}的某个列名是{@code null}或者只包含空白字符。
     */
    private String insertSql(
            String tableName,
            String[] columnNames
    ) {
        tableName = notBlank(tableName, "tableName").trim().toUpperCase();
        notEmpty(columnNames, "columnNames");

        final StringBuilder sb = new StringBuilder();
        sb.append("INSERT INTO ").append(wrap(tableName)).append(" (");

        for (int i = 0; i < columnNames.length; ++i) {
            sb.append(wrap(columnNames[i]));
            if (i < columnNames.length - 1) {
                sb.append(",");
            }
        }
        sb.append(") VALUES (");

        for (int i = 0; i < columnNames.length; ++i) {
            sb.append("?");
            if (i < columnNames.length - 1) {
                sb.append(",");
            }
        }
        sb.append(")");

        return sb.toString();
    }

    /**
     * 创建执行清除表所有数据的SQL命令。
     *
     * @param tableName
     *         表名。
     * @return 用于清除指定表的所有记录的SQL命令。
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符。
     */
    private String cleanSql(String tableName) {
        tableName = notBlank(tableName, "tableName").trim().toUpperCase();
        return "DELETE FROM " + wrap(tableName);
    }

    /**
     * 构造从表中查询数据的SQL语句
     *
     * @param tableName
     *         表名
     * @param predicate
     *         条件
     * @return 用于查询数据的SQL语句。
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符。
     */
    private String selectSql(
            String tableName,
            String predicate
    ) {
        tableName = notBlank(tableName, "tableName").trim();
        String sql = "SELECT * FROM " + wrap(tableName);
        return predicate.isEmpty() ? sql : (sql + " WHERE " + predicate);
    }

    /**
     * 获取指定表的所有列的元数据。
     *
     * @param tableName
     *         表名。
     * @return 指定表的所有列的元数据。
     * @throws DbException
     *         如果执行SQL命令获取元数据时出错。
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符。
     */
    private DbUnitHelper.Column[] metaData(String tableName) {
        tableName = notBlank(tableName, "tableName").trim().toUpperCase();

        try (final Connection connection = connection()) {
            try (final Statement statement = connection.createStatement()) {
                statement.setFetchDirection(ResultSet.FETCH_FORWARD);
                statement.setFetchSize(1);

                final String sql = "SELECT * FROM " + tableName + " WHERE 0 = 1";
                try (final ResultSet resultSet = statement.executeQuery(sql)) {
                    final ResultSetMetaData meta = resultSet.getMetaData();
                    final int c = meta.getColumnCount();
                    final DbUnitHelper.Column[] ret = new DbUnitHelper.Column[c];

                    for (int i = 0; i < c; ++i) {
                        ret[i] = new DbUnitHelper.Column(meta.getColumnName(i + 1), meta.getColumnType(i + 1));
                    }

                    connection.rollback();

                    return ret;
                }
            }
        } catch (SQLException ex) {
            throw new DbException(ex);
        }
    }

    /**
     * 按照条件从指定的数据库表中查询数据。
     *
     * @param tableName
     *         表名
     * @param predicate
     *         条件
     * @return 符合条件的数据
     * @throws DbException
     *         如果执行查询时出现数据库错误。
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符，或者从未获取过指定表的列的元数据。
     * @see #loadMetaData(String)
     * @see #load(String)
     */
    private DbUnitHelper.Row[] resultSet(
            String tableName,
            String predicate
    ) {
        tableName = notBlank(tableName, "tableName").trim();
        final DbUnitHelper.Column[] columns = notNull(this.tables.get(tableName), tableName + ".columns");

        try (final Connection connection = connection()) {
            try (final Statement statement = connection.createStatement()) {
                statement.setFetchDirection(ResultSet.FETCH_FORWARD);
                statement.setFetchSize(0);

                final String sql = selectSql(tableName, predicate.trim());
                try (final ResultSet resultSet = statement.executeQuery(sql)) {

                    final List<DbUnitHelper.Row> rowList = new ArrayList<>();
                    while (resultSet.next()) {
                        final String[] values = new String[resultSet.getMetaData().getColumnCount()];

                        for (int i = 0; i < values.length; ++i) {
                            values[i] = content(resultSet, i + 1, columns[i].getType());
                        }

                        rowList.add(new DbUnitHelper.Row(values));
                    }

                    connection.rollback();

                    return rowList.toArray(new DbUnitHelper.Row[0]);
                }
            }
        } catch (SQLException ex) {
            throw new DbException(ex);
        }
    }

    /**
     * 从结果集中获取当前行的指定列的数据，并转化为字符串形式。
     *
     * @param resultSet
     *         包含数据的结果集。
     * @param columnIndex
     *         列序号（从1开始）。
     * @return 指定列包含的数据。
     * @throws SQLException
     *         从结果集中获取数据出错，或者列序号不合法。
     * @throws IllegalArgumentException
     *         如果参数{@code resultSet}是{@code null}。
     */
    private String content(
            ResultSet resultSet,
            int columnIndex,
            int columnType
    )
            throws SQLException {
        notNull(resultSet, "resultSet");

        switch (columnType) {
            case Types.ARRAY:
                final Object[] aryValue = (Object[]) (resultSet.getArray(columnIndex).getArray());
                return resultSet.wasNull() ? "" : join(aryValue, ',');
            case Types.BIGINT:
            case Types.DECIMAL:
            case Types.NUMERIC:
                final BigDecimal decimalValue = resultSet.getBigDecimal(columnIndex);
                return resultSet.wasNull() ? "" : decimalValue.toString();
            case Types.BINARY:
            case Types.BLOB:
            case Types.VARBINARY:
            case Types.LONGVARBINARY:
                final byte[] byValue = resultSet.getBytes(columnIndex);
                return resultSet.wasNull() ? "" : getEncoder().encodeToString(byValue);
            case Types.BIT:
            case Types.BOOLEAN:
                final boolean blValue = resultSet.getBoolean(columnIndex);
                return resultSet.wasNull() ? "" : blValue ? "true" : "false";
            case Types.CHAR:
                final String chValue = resultSet.getString(columnIndex);
                return resultSet.wasNull() ? "" : substring(chValue, 0, 1);
            case Types.DATE:
                final java.sql.Date dateValue = resultSet.getDate(columnIndex);
                return resultSet.wasNull() ? "" : format(dateValue, "yyyy-MM-dd");
            case Types.REAL:
            case Types.DOUBLE:
                final double dbValue = resultSet.getDouble(columnIndex);
                return resultSet.wasNull() ? "" : String.valueOf(dbValue);
            case Types.FLOAT:
                final double flValue = resultSet.getFloat(columnIndex);
                return resultSet.wasNull() ? "" : String.valueOf(flValue);
            case Types.INTEGER:
                final int intValue = resultSet.getInt(columnIndex);
                return resultSet.wasNull() ? "" : String.valueOf(intValue);
            case Types.SMALLINT:
                final short shValue = resultSet.getShort(columnIndex);
                return resultSet.wasNull() ? "" : String.valueOf(shValue);
            case Types.TIME:
                final java.sql.Time timeValue = resultSet.getTime(columnIndex);
                return resultSet.wasNull() ? "" : format(timeValue, "HH:mm:ss");
            case Types.TIMESTAMP:
                final Timestamp tsValue = resultSet.getTimestamp(columnIndex);
                return resultSet.wasNull() ? "" : format(tsValue, "yyyy-MM-dd HH:mm:ss");
            case Types.TINYINT:
                final byte btValue = resultSet.getByte(columnIndex);
                return resultSet.wasNull() ? "" : String.valueOf(btValue);
            case Types.VARCHAR:
            case Types.NCHAR:
            case Types.NVARCHAR:
            case Types.LONGVARCHAR:
            case Types.LONGNVARCHAR:
                final String sValue = resultSet.getString(columnIndex);
                return resultSet.wasNull() ? "" : sValue;
            default:
                final Object objValue = resultSet.getObject(columnIndex);
                return resultSet.wasNull() ? "" : String.valueOf(objValue);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readXml(
            String tableName,
            InputStream stream
    )
            throws XMLStreamException {
        tableName = notBlank(tableName, "tableName").trim();
        notNull(stream, "stream");

        final DbUnitHelper.Column[] columns = this.tables.get(tableName);
        if (columns == null) {
            throw new IllegalArgumentException("illegal table name: " + tableName);
        }

        final DbUnitHelper.Row[] rows = DbUnitTableReader.read(stream)
                                                         .stream()
                                                         .map(row -> {
                                                             final String[] values = new String[columns.length];

                                                             for (final Map.Entry<String, String> item : row.entrySet()) {
                                                                 for (int j = 0; j < columns.length; ++j) {
                                                                     if (columns[j].getName().equalsIgnoreCase(item.getKey())) {
                                                                         values[j] = item.getValue();
                                                                     }
                                                                 }
                                                             }

                                                             return new DbUnitHelper.Row(values);
                                                         }).toArray(DbUnitHelper.Row[]::new);

        logger.debug("read {} row(s) for {}", rows.length, tableName);

        this.tableRows.put(tableName, rows);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void loadMetaData(
            String tableName
    ) {
        tableName = notBlank(tableName, "tableName").trim();

        this.tables.put(tableName, metaData(tableName));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void load(
            String tableName
    ) {
        load(tableName, "");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void load(
            String tableName,
            String predicate
    ) {
        tableName = notBlank(tableName, "tableName").trim();

        this.tableRows.put(tableName, resultSet(tableName, predicate));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeXml(
            String tableName,
            OutputStream stream
    )
            throws XMLStreamException {
        tableName = notBlank(tableName, "tableName").trim();

        final DbUnitHelper.Column[] columns = notNull(this.tables.get(tableName), tableName + ".columns");

        DbUnitTableWriter.write(tableName, columns, this.tableRows.get(tableName), stream);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void ddl(
            String sql
    ) {
        logger.trace("DDL SQL: " + sql);

        try (final Connection connection = connection()) {
            try (final Statement statement = connection.createStatement()) {
                statement.execute(sql);

                connection.commit();
            }
        } catch (SQLException ex) {
            throw new DbException(ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void insertAll(
            boolean clean
    ) {
        final String[] tableNames = tables.keySet().toArray(new String[0]);

        if (clean) {
            for (int i = tableNames.length - 1; i >= 0; --i) {
                clear(tableNames[i]);
            }
        }

        for (final String tableName : tableNames) {
            final DbUnitHelper.Column[] columns = this.tables.get(tableName);
            final DbUnitHelper.Row[] rows = this.tableRows.get(tableName);

            insert(tableName, columns, rows);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void clearAll() {
        try (final Connection connection = connection()) {
            try (final Statement statement = connection.createStatement()) {

                // Clear all tables.
                for (final String tableName : tables.keySet()) {
                    statement.executeUpdate(cleanSql(tableName));
                }

                connection.commit();
            }
        } catch (SQLException ex) {
            throw new DbException(ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object[] executeQuery(
            String sql,
            Object... parameters
    ) {
        final List<Object[]> rows = this.executeQueryList(sql, parameters);
        if (rows == null) {
            return null;
        } else if (rows.size() == 0) {
            return null;
        } else {
            return rows.get(0);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Object[]> executeQueryList(
            String sql,
            Object... parameters
    ) {
        try (final Connection connection = connection()) {
            try (final PreparedStatement statement = connection.prepareStatement(sql)) {
                for (int i = 0; i < parameters.length; ++i) {
                    final Object parameter = parameters[i];

                    if (parameter == null) {
                        throw new NullPointerException("Parameters[" + i + "] is null");
                    }

                    if (parameter instanceof Integer) {
                        statement.setInt(i + 1, (Integer) parameter);
                    } else if (parameter instanceof Long) {
                        statement.setBigDecimal(i + 1, BigDecimal.valueOf(((Long) parameter)));
                    } else if (parameter instanceof BigDecimal) {
                        statement.setBigDecimal(i + 1, (BigDecimal) parameter);
                    } else if (parameter instanceof BigInteger) {
                        statement.setBigDecimal(i + 1, new BigDecimal(((BigInteger) parameter)));
                    } else if (parameter instanceof Byte) {
                        statement.setByte(i + 1, (Byte) parameter);
                    } else if (parameter instanceof Short) {
                        statement.setShort(i + 1, (Short) parameter);
                    } else if (parameter instanceof Double) {
                        statement.setDouble(i + 1, (Double) parameter);
                    } else if (parameter instanceof Float) {
                        statement.setFloat(i + 1, (Float) parameter);
                    } else {
                        statement.setString(i + 1, String.valueOf(parameter));
                    }
                }

                try (final ResultSet resultSet = statement.executeQuery()) {
                    final ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
                    final int l = resultSetMetaData.getColumnCount();

                    final List<Object[]> ret = new ArrayList<>();
                    while (resultSet.next()) {
                        final Object[] row = new Object[l];

                        for (int i = 0; i < l; ++i) {
                            final int columnType = resultSetMetaData.getColumnType(i + 1);
                            row[i] = getRowValue(resultSet, i + 1, columnType);
                        }

                        ret.add(row);
                    }

                    connection.rollback();
                    return ret;
                }
            }
        } catch (SQLException ex) {
            throw new DbException(ex);
        }
    }

    /**
     * 获取结果集当前行的指定列的数据。
     *
     * @param resultSet
     *         结果集。
     * @param columnIndex
     *         列序号（从1开始）。
     * @param columnType
     *         列类型。
     * @return 结果集当前行的指定列的数据。
     * @throws IllegalArgumentException
     *         如果参数{@code resultSet}是{@code null}。
     * @throws SQLException
     *         如果获取数据出错。
     */
    private Object getRowValue(
            ResultSet resultSet,
            int columnIndex,
            int columnType
    )
            throws SQLException {
        switch (columnType) {
            case Types.TIMESTAMP:
                return new Date(resultSet.getTimestamp(columnIndex).getTime());
            case Types.DATE:
                return new Date(resultSet.getDate(columnIndex).getTime());
            case Types.TIME:
                return new Date(resultSet.getTime(columnIndex).getTime());
            case Types.BLOB:
            case Types.BINARY:
                final java.sql.Blob blob = resultSet.getBlob(columnIndex);
                try {
                    final long blobLength = blob.length();
                    return blob.getBytes(1L, (int) blobLength);
                } finally {
                    blob.free();
                }
            case Types.VARCHAR:
            case Types.NCHAR:
            case Types.NVARCHAR:
            case Types.LONGVARCHAR:
            case Types.LONGNVARCHAR:
                return trimToEmpty(resultSet.getString(columnIndex));
            default:
                return resultSet.getObject(columnIndex);
        }
    }

    /**
     * 向指定的表中插入记录。
     *
     * @param tableName
     *         表名。
     * @param columns
     *         所有待插入的列的元数据。
     * @param rows
     *         所有待插入的行的数据。
     * @throws DbException
     *         执行INSERT命令插入数据时出错。
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符，或者参数{@code columnNames}是{@code null}或者长度为0，
     *         或者参数{@code columnNames}的某个列名是{@code null}或者只包含空白字符。
     */

    private void insert(
            String tableName,
            DbUnitHelper.Column[] columns,
            DbUnitHelper.Row[] rows
    ) {
        tableName = notBlank(tableName, "tableName").trim();
        notEmpty(columns, "columns");
        notNull(rows, "rows");

        final String sql = insertSql(tableName, Stream.of(columns).map(DbUnitHelper.Column::getName).toArray(String[]::new));
        logger.trace("INSERT SQL: " + sql);

        if (rows.length == 0) {
            logger.debug("no rows found of {}", tableName);
            return;
        }

        for (final DbUnitHelper.Row row : rows) {
            if (row.size() < columns.length) {
                throw new IllegalStateException("expected " + columns.length + " values, but " + row.size() + " got");
            }
        }

        try (final Connection connection = connection()) {
            try (final PreparedStatement statement = connection.prepareStatement(sql)) {
                for (final DbUnitHelper.Row row : rows) {
                    for (int i = 0; i < columns.length; ++i) {
                        final Object obj = row.get(i);
                        try {
                            parameter(statement, i + 1, columns[i].getType(), row.get(i));
                        } catch (ParseException ex) {
                            logger.warn("cannot create parameter[{}] of {}, {}", (i + 1), obj, dumpException(ex));
                        }
                    }
                    statement.executeUpdate();
                }

                connection.commit();

                logger.debug("inserted {} row(s) into {}", rows.length, tableName);
            }
        } catch (SQLException ex) {
            throw new DbException(ex);
        }
    }

    /**
     * 清除指定表的所有数据。
     *
     * @param tableName
     *         表名。
     * @throws DbException
     *         执行DELETE命令清除表的数据时出错。
     * @throws IllegalArgumentException
     *         如果参数{@code tableName}是{@code null}或者只包含空白字符。
     */
    private void clear(
            String tableName
    ) {
        tableName = notBlank(tableName, "tableName").trim();

        try (final Connection connection = connection()) {
            try (final Statement statement = connection.createStatement()) {
                final int c = statement.executeUpdate(cleanSql(tableName));

                logger.debug("deleted {} row(s) from {}", c, tableName);

                connection.commit();
            }
        } catch (SQLException ex) {
            throw new DbException(ex);
        }
    }
}
